/*
 *  Copyright (c) 2012 Michael Zucchi
 *
 *  This file is part of ReaderZ, a Java e-ink reader application.
 *
 *  ReaderZ is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  ReaderZ is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with ReaderZ.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

#include <linux/mxcfb.h>

#include "EInkFB.h"

// jni helpers
#define str_get(jstr) (*env)->GetStringUTFChars(env, jstr, NULL)
#define str_release(jstr, str) (*env)->ReleaseStringUTFChars(env, jstr, str)

#define buf_wrap(mem, len) (*env)->NewDirectByteBuffer(env, mem, len)
#define buf_addr(jp) (jp != NULL ? (*env)->GetDirectBufferAddress(env, jp) : NULL)
#define buf_len(jp) (jp != NULL ? (*env)->GetDirectBufferCatpacity(env, jp) : 0)

struct fbdev {
	int fbfd;
	struct fb_var_screeninfo vinfo;
	struct fb_fix_screeninfo finfo;
	// mapped frame buffer address
	unsigned short int *fb;
	size_t fb_size;
};

static void refresh(struct fbdev *fb, int mode, int flags, int marker, int x, int y, int w, int h) {
	struct mxcfb_update_data region;

	region.update_region.top = y;
	region.update_region.left = x;
	region.update_region.width = w;
	region.update_region.height = h;
	region.waveform_mode = WAVEFORM_MODE_AUTO;
	region.update_mode = mode;
	region.update_marker = marker;
	region.temp = TEMP_USE_AMBIENT;
	region.flags = flags;

	if (ioctl(fb->fbfd, MXCFB_SEND_UPDATE, &region) == -1) {
		perror("Error sending update information");
	}
}

static void wait_refresh(struct fbdev *fb, int marker) {
	if (ioctl(fb->fbfd, MXCFB_WAIT_FOR_UPDATE_COMPLETE, &marker) == -1) {
		perror("Error waiting for update");
	}
}

JNIEXPORT jobject JNICALL Java_EInkFB_open
(JNIEnv *env, jclass jc, jstring jname) {
	const char *name = str_get(jname);
	struct fbdev *fb;

	// Open the file for reading and writing
	int fbfd = open(name, O_RDWR);

	if (fbfd == -1) {
		perror("Error opening fb device");
		goto fail;
	}
	printf("The framebuffer device was opened successfully.\n");

	fb = malloc(sizeof(*fb));

	fb->fbfd = fbfd;
	fb->fb = NULL;

	// Get fixed screen information
	if (ioctl(fbfd, FBIOGET_FSCREENINFO, &fb->finfo) == -1) {
		perror("Error reading fixed information");
		goto fail1;
	}

	// Get variable screen information
	if (ioctl(fbfd, FBIOGET_VSCREENINFO, &fb->vinfo) == -1) {
		perror("Error reading variable information");
		goto fail1;
	}

	// Configure for what we actually want
	fb->vinfo.bits_per_pixel = 8;
	fb->vinfo.grayscale = 1;
	// 0 is landscape right handed, 3 is portrait
	fb->vinfo.rotate = 3;

	if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &fb->vinfo) == -1) {
		perror("Error configuring display");
		goto fail1;
	}

	printf(" %dx%d, %dbpp virtual %dx%d rotate%d\n", fb->vinfo.xres, fb->vinfo.yres, fb->vinfo.bits_per_pixel, fb->vinfo.xres_virtual, fb->vinfo.yres_virtual, fb->vinfo.rotate);

	// Figure out the size of the screen in bytes
	fb->fb_size = fb->vinfo.xres_virtual * fb->vinfo.yres_virtual * fb->vinfo.bits_per_pixel / 8;

	// Map the device to memory
	fb->fb = mmap(0, fb->fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
	if (fb->fb == MAP_FAILED) {
		perror("Error: failed to map framebuffer device to memory");
		goto fail1;
	}

	return buf_wrap(fb, sizeof(fb));
 fail1:
	close(fb->fbfd);
	free(fb);
 fail:
	str_release(jname, name);

	return NULL;
}

JNIEXPORT jobject JNICALL Java_EInkFB_getBuffer
(JNIEnv *env, jclass jc, jobject jptr) {
	struct fbdev *fb = buf_addr(jptr);

	return buf_wrap(fb->fb, fb->fb_size);
}

JNIEXPORT jint JNICALL Java_EInkFB_getWidth
(JNIEnv *env, jclass jc, jobject jfb) {
	struct fbdev *fb = buf_addr(jfb);

	return fb->vinfo.xres;
}

JNIEXPORT jint JNICALL Java_EInkFB_getHeight
(JNIEnv *env, jclass jc, jobject jfb) {
	struct fbdev *fb = buf_addr(jfb);

	return fb->vinfo.yres;
}

JNIEXPORT jint JNICALL Java_EInkFB_getVirtualWidth
(JNIEnv *env, jclass jc, jobject jfb) {
	struct fbdev *fb = buf_addr(jfb);

	return fb->vinfo.xres_virtual;
}

JNIEXPORT jint JNICALL Java_EInkFB_getVirtualHeight
(JNIEnv *env, jclass jc, jobject jfb) {
	struct fbdev *fb = buf_addr(jfb);

	return fb->vinfo.xres_virtual;
}

JNIEXPORT void JNICALL Java_EInkFB_update
(JNIEnv *env, jclass jc, jobject jptr, jint mode, int marker, jint l , jint t, jint w, jint h, jint flags) {
	struct fbdev *fb = buf_addr(jptr);
	int uflags = 0;

	if (flags & 1)
		uflags |= EPDC_FLAG_ENABLE_INVERSION;
	if (flags & 2)
		uflags |= EPDC_FLAG_FORCE_MONOCHROME;;

	// FIXME: no-op on 0 size, otherwise it crashes the driver

	refresh(fb, mode, uflags, marker, l, t, w, h);

	if (flags & 0x80)
		wait_refresh(fb, marker);
}

static int ioc_set[] = {
	MXCFB_SET_TEMPERATURE,
	MXCFB_SET_AUTO_UPDATE_MODE,
	MXCFB_WAIT_FOR_UPDATE_COMPLETE,
	MXCFB_SET_PWRDOWN_DELAY,
	MXCFB_SET_UPDATE_SCHEME,
	MXCFB_SET_BORDER_MODE,
	MXCFB_SET_EPD_PWR0_CTRL,
	MXCFB_SET_EPD_PWR2_CTRL,
	MXCFB_SET_TEMP_AUTO_UPDATE_PERIOD,
	MXCFB_SET_MERGE_ON_WAVEFORM_MISMATCH
};
/*
static int ioc_get[] = {
	MXCFB_GET_PMIC_TEMPERATURE,
	0,
	0,
	MXCFB_GET_PWRDOWN_DELAY,
	0,
	0,
	0,
	0,
	0,
	0
};
*/
#define IOC_SIZEOF (sizeof(ioc_set)/sizeof(ioc_set[0]))

JNIEXPORT jint JNICALL Java_EInkFB_ioctlSetInteger
(JNIEnv *env, jclass jc, jobject jptr, jint code, jint jvalue) {
	struct fbdev *fb = buf_addr(jptr);
	int ioc;
	jint res = -1;
	int32_t value = jvalue;

	if (code < IOC_SIZEOF && (ioc = ioc_set[code]) != 0) {
		if ((res = ioctl(fb->fbfd, ioc, &value)) == -1)
			perror("Error in ioctl");
	} else {
		fprintf(stderr, "Invalid ioctl number %d\n", code);
	}

	return res;
}

// Not really useful this ...
/*
JNIEXPORT jint JNICALL Java_au_notzed_fb_EInkFB_ioctlGetInteger
(JNIEnv *env, jclass jc, jobject jptr, jint code) {
	struct fbdev *fb = buf_addr(jptr);
	int ioc;
	jint res = -1;
	int32_t value = -1;

	if (code < IOC_SIZEOF && (ioc = ioc_get[code]) != 0) {
		if ((res = ioctl(fb->fbfd, ioc, &value)) == -1)
			perror("Error in ioctl");
	} else {
		fprintf(stderr, "Invalid ioctl number %d\n", code);
	}

	return value;
	}*/

JNIEXPORT void JNICALL Java_EInkFB_close
(JNIEnv *env, jclass jc, jobject jptr) {
	struct fbdev *fb = buf_addr(jptr);

	munmap(fb->fb, fb->fb_size);
	close(fb->fbfd);
	free(fb);
}
